//! Type mapping utilities for converting between SQL and GraphQL types
//! 
//! This module provides functionality to map SQL data types to GraphQL types
//! and generate appropriate input/output types.

use crate::{
    models::{GraphQLFieldInfo, GraphQLTypeInfo, GraphQLTypeKind},
    Error, Result,
};
use std::collections::HashMap;

/// Type mapper for converting between SQL and GraphQL types
pub struct TypeMapper {
    /// Custom type mappings
    custom_mappings: HashMap<String, String>,
}

impl TypeMapper {
    /// Create a new type mapper
    pub fn new() -> Self {
        Self {
            custom_mappings: HashMap::new(),
        }
    }
    
    /// Add a custom type mapping
    pub fn add_custom_mapping(&mut self, sql_type: String, graphql_type: String) {
        self.custom_mappings.insert(sql_type, graphql_type);
    }
    
    /// Map SQL type to GraphQL type
    pub fn map_sql_to_graphql(&self, sql_type: &str) -> String {
        // Check custom mappings first
        if let Some(custom_type) = self.custom_mappings.get(sql_type) {
            return custom_type.clone();
        }
        
        // Standard mappings
        match sql_type.to_lowercase().as_str() {
            // Integer types
            "int" | "integer" | "int4" | "serial" | "serial4" => "Int".to_string(),
            "bigint" | "int8" | "bigserial" | "serial8" => "Int".to_string(),
            "smallint" | "int2" | "smallserial" | "serial2" => "Int".to_string(),
            "tinyint" => "Int".to_string(),
            
            // Float types
            "real" | "float4" => "Float".to_string(),
            "double precision" | "float8" | "double" | "float" => "Float".to_string(),
            "decimal" | "numeric" => "Float".to_string(),
            
            // String types
            "varchar" | "character varying" | "char" | "character" => "String".to_string(),
            "text" | "longtext" | "mediumtext" | "tinytext" => "String".to_string(),
            "enum" => "String".to_string(),
            
            // Boolean types
            "boolean" | "bool" | "bit" => "Boolean".to_string(),
            
            // Date/Time types
            "timestamp" | "timestamptz" | "timestamp with time zone" => "DateTime".to_string(),
            "timestamp without time zone" => "DateTime".to_string(),
            "date" => "Date".to_string(),
            "time" | "timetz" | "time with time zone" => "Time".to_string(),
            "time without time zone" => "Time".to_string(),
            "datetime" => "DateTime".to_string(),
            
            // JSON types
            "json" | "jsonb" => "JSON".to_string(),
            
            // UUID types
            "uuid" => "ID".to_string(),
            
            // Binary types
            "bytea" | "blob" | "binary" | "varbinary" => "String".to_string(),
            
            // Default to String for unknown types
            _ => {
                tracing::warn!("Unknown SQL type '{}', mapping to String", sql_type);
                "String".to_string()
            }
        }
    }
    
    /// Map SQL type to Rust type
    pub fn map_sql_to_rust(&self, sql_type: &str) -> String {
        match sql_type.to_lowercase().as_str() {
            // Integer types
            "int" | "integer" | "int4" | "serial" | "serial4" => "i32".to_string(),
            "bigint" | "int8" | "bigserial" | "serial8" => "i64".to_string(),
            "smallint" | "int2" | "smallserial" | "serial2" => "i16".to_string(),
            "tinyint" => "i8".to_string(),
            
            // Float types
            "real" | "float4" => "f32".to_string(),
            "double precision" | "float8" | "double" | "float" => "f64".to_string(),
            "decimal" | "numeric" => "rust_decimal::Decimal".to_string(),
            
            // String types
            "varchar" | "character varying" | "char" | "character" => "String".to_string(),
            "text" | "longtext" | "mediumtext" | "tinytext" => "String".to_string(),
            "enum" => "String".to_string(),
            
            // Boolean types
            "boolean" | "bool" | "bit" => "bool".to_string(),
            
            // Date/Time types
            "timestamp" | "timestamptz" | "timestamp with time zone" => "chrono::DateTime<chrono::Utc>".to_string(),
            "timestamp without time zone" => "chrono::NaiveDateTime".to_string(),
            "date" => "chrono::NaiveDate".to_string(),
            "time" | "timetz" | "time with time zone" => "chrono::NaiveTime".to_string(),
            "time without time zone" => "chrono::NaiveTime".to_string(),
            "datetime" => "chrono::DateTime<chrono::Utc>".to_string(),
            
            // JSON types
            "json" | "jsonb" => "serde_json::Value".to_string(),
            
            // UUID types
            "uuid" => "uuid::Uuid".to_string(),
            
            // Binary types
            "bytea" | "blob" | "binary" | "varbinary" => "Vec<u8>".to_string(),
            
            // Default to String for unknown types
            _ => "String".to_string(),
        }
    }
    
    /// Generate filter input type for a table
    pub fn generate_filter_input(&self, table_name: &str, fields: &[GraphQLFieldInfo]) -> GraphQLTypeInfo {
        let mut filter_fields = Vec::new();
        
        for field in fields {
            // Add basic comparison operators
            filter_fields.push(GraphQLFieldInfo {
                name: field.name.clone(),
                field_type: format!("{}Filter", field.field_type),
                is_required: false,
                is_list: false,
                description: Some(format!("Filter by {}", field.name)),
                default_value: None,
            });
            
            // Add _eq, _ne, _in, _not_in operators
            filter_fields.push(GraphQLFieldInfo {
                name: format!("{}_eq", field.name),
                field_type: field.field_type.clone(),
                is_required: false,
                is_list: false,
                description: Some(format!("Equal to {}", field.name)),
                default_value: None,
            });
            
            filter_fields.push(GraphQLFieldInfo {
                name: format!("{}_ne", field.name),
                field_type: field.field_type.clone(),
                is_required: false,
                is_list: false,
                description: Some(format!("Not equal to {}", field.name)),
                default_value: None,
            });
            
            filter_fields.push(GraphQLFieldInfo {
                name: format!("{}_in", field.name),
                field_type: field.field_type.clone(),
                is_required: false,
                is_list: true,
                description: Some(format!("In list for {}", field.name)),
                default_value: None,
            });
            
            // Add string-specific operators
            if field.field_type == "String" {
                filter_fields.push(GraphQLFieldInfo {
                    name: format!("{}_contains", field.name),
                    field_type: "String".to_string(),
                    is_required: false,
                    is_list: false,
                    description: Some(format!("Contains substring for {}", field.name)),
                    default_value: None,
                });
                
                filter_fields.push(GraphQLFieldInfo {
                    name: format!("{}_starts_with", field.name),
                    field_type: "String".to_string(),
                    is_required: false,
                    is_list: false,
                    description: Some(format!("Starts with for {}", field.name)),
                    default_value: None,
                });
                
                filter_fields.push(GraphQLFieldInfo {
                    name: format!("{}_ends_with", field.name),
                    field_type: "String".to_string(),
                    is_required: false,
                    is_list: false,
                    description: Some(format!("Ends with for {}", field.name)),
                    default_value: None,
                });
            }
            
            // Add numeric comparison operators
            if matches!(field.field_type.as_str(), "Int" | "Float") {
                for op in &["_gt", "_gte", "_lt", "_lte"] {
                    filter_fields.push(GraphQLFieldInfo {
                        name: format!("{}{}", field.name, op),
                        field_type: field.field_type.clone(),
                        is_required: false,
                        is_list: false,
                        description: Some(format!("Comparison {} for {}", op, field.name)),
                        default_value: None,
                    });
                }
            }
        }
        
        // Add logical operators
        filter_fields.push(GraphQLFieldInfo {
            name: "_and".to_string(),
            field_type: format!("{}Filter", table_name),
            is_required: false,
            is_list: true,
            description: Some("Logical AND".to_string()),
            default_value: None,
        });
        
        filter_fields.push(GraphQLFieldInfo {
            name: "_or".to_string(),
            field_type: format!("{}Filter", table_name),
            is_required: false,
            is_list: true,
            description: Some("Logical OR".to_string()),
            default_value: None,
        });
        
        filter_fields.push(GraphQLFieldInfo {
            name: "_not".to_string(),
            field_type: format!("{}Filter", table_name),
            is_required: false,
            is_list: false,
            description: Some("Logical NOT".to_string()),
            default_value: None,
        });
        
        GraphQLTypeInfo {
            name: format!("{}Filter", table_name),
            kind: GraphQLTypeKind::Input,
            description: Some(format!("Filter input for {}", table_name)),
            fields: filter_fields,
            enum_values: Vec::new(),
        }
    }
    
    /// Generate order input type for a table
    pub fn generate_order_input(&self, table_name: &str, fields: &[GraphQLFieldInfo]) -> GraphQLTypeInfo {
        let mut order_fields = Vec::new();
        
        for field in fields {
            order_fields.push(GraphQLFieldInfo {
                name: field.name.clone(),
                field_type: "OrderDirection".to_string(),
                is_required: false,
                is_list: false,
                description: Some(format!("Order by {}", field.name)),
                default_value: None,
            });
        }
        
        GraphQLTypeInfo {
            name: format!("{}OrderBy", table_name),
            kind: GraphQLTypeKind::Input,
            description: Some(format!("Order input for {}", table_name)),
            fields: order_fields,
            enum_values: Vec::new(),
        }
    }
    
    /// Generate order direction enum
    pub fn generate_order_direction_enum(&self) -> GraphQLTypeInfo {
        GraphQLTypeInfo {
            name: "OrderDirection".to_string(),
            kind: GraphQLTypeKind::Enum,
            description: Some("Order direction".to_string()),
            fields: Vec::new(),
            enum_values: vec!["ASC".to_string(), "DESC".to_string()],
        }
    }
}

impl Default for TypeMapper {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_sql_to_graphql_mapping() {
        let mapper = TypeMapper::new();
        
        assert_eq!(mapper.map_sql_to_graphql("integer"), "Int");
        assert_eq!(mapper.map_sql_to_graphql("varchar"), "String");
        assert_eq!(mapper.map_sql_to_graphql("boolean"), "Boolean");
        assert_eq!(mapper.map_sql_to_graphql("timestamp"), "DateTime");
        assert_eq!(mapper.map_sql_to_graphql("uuid"), "ID");
        assert_eq!(mapper.map_sql_to_graphql("unknown_type"), "String");
    }
    
    #[test]
    fn test_sql_to_rust_mapping() {
        let mapper = TypeMapper::new();
        
        assert_eq!(mapper.map_sql_to_rust("integer"), "i32");
        assert_eq!(mapper.map_sql_to_rust("varchar"), "String");
        assert_eq!(mapper.map_sql_to_rust("boolean"), "bool");
        assert_eq!(mapper.map_sql_to_rust("timestamp"), "chrono::DateTime<chrono::Utc>");
        assert_eq!(mapper.map_sql_to_rust("uuid"), "uuid::Uuid");
    }
    
    #[test]
    fn test_custom_mapping() {
        let mut mapper = TypeMapper::new();
        mapper.add_custom_mapping("custom_type".to_string(), "CustomGraphQLType".to_string());
        
        assert_eq!(mapper.map_sql_to_graphql("custom_type"), "CustomGraphQLType");
        assert_eq!(mapper.map_sql_to_graphql("integer"), "Int");
    }
    
    #[test]
    fn test_filter_input_generation() {
        let mapper = TypeMapper::new();
        let fields = vec![
            GraphQLFieldInfo {
                name: "id".to_string(),
                field_type: "Int".to_string(),
                is_required: true,
                is_list: false,
                description: None,
                default_value: None,
            },
            GraphQLFieldInfo {
                name: "name".to_string(),
                field_type: "String".to_string(),
                is_required: false,
                is_list: false,
                description: None,
                default_value: None,
            },
        ];
        
        let filter = mapper.generate_filter_input("User", &fields);
        assert_eq!(filter.name, "UserFilter");
        assert_eq!(filter.kind, GraphQLTypeKind::Input);
        assert!(!filter.fields.is_empty());
        
        // Check that string-specific operators are included
        let contains_field = filter.fields.iter().find(|f| f.name == "name_contains");
        assert!(contains_field.is_some());
        
        // Check that numeric operators are included
        let gt_field = filter.fields.iter().find(|f| f.name == "id_gt");
        assert!(gt_field.is_some());
    }
    
    #[test]
    fn test_order_input_generation() {
        let mapper = TypeMapper::new();
        let fields = vec![
            GraphQLFieldInfo {
                name: "id".to_string(),
                field_type: "Int".to_string(),
                is_required: true,
                is_list: false,
                description: None,
                default_value: None,
            },
            GraphQLFieldInfo {
                name: "name".to_string(),
                field_type: "String".to_string(),
                is_required: false,
                is_list: false,
                description: None,
                default_value: None,
            },
        ];
        
        let order = mapper.generate_order_input("User", &fields);
        assert_eq!(order.name, "UserOrderBy");
        assert_eq!(order.kind, GraphQLTypeKind::Input);
        assert_eq!(order.fields.len(), 2);
        
        for field in &order.fields {
            assert_eq!(field.field_type, "OrderDirection");
        }
    }
    
    #[test]
    fn test_order_direction_enum() {
        let mapper = TypeMapper::new();
        let enum_type = mapper.generate_order_direction_enum();
        
        assert_eq!(enum_type.name, "OrderDirection");
        assert_eq!(enum_type.kind, GraphQLTypeKind::Enum);
        assert_eq!(enum_type.enum_values, vec!["ASC", "DESC"]);
    }
}